package com.trg.flex.remote
{
	import com.trg.flex.FlexUtils;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.binding.utils.ChangeWatcher;
	import mx.collections.ArrayCollection;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.messaging.messages.CommandMessage;
	
	public class RemoteCallManager extends EventDispatcher
	{
		//The constructor can not be used externally. The singleton instance of RemoteCallManager should be obtained
		//through the static instance() method.
		public function RemoteCallManager()
		{
			if (!_instanting) throw new Error("Use RemoteCallManager.instance() to get instance of RemoteCallManager.");
			_pendingCallsByQueue = new PendingCallsByQueue(queues);
			_reconnectStrategy = new ReconnectStrategy(this);
		}
		
		private static var _instanting:Boolean = false;
		private static var _instance:RemoteCallManager;
		
		public static function instance():RemoteCallManager {
			if (_instance == null) {
				_instanting = true;
				_instance = new RemoteCallManager();
				_instanting = false;
			}
			return _instance;
		}
		
		// ----------------------- PUBLIC INTERFACE ----------------------------
		
		public function call(call:RemoteCall, strategy:String = null, queue:String = null):void {
			if (call.status == RemoteCall.STATUS_PENDING)
				throw new Error("A call cannot be re-called while it is currently pending.");
			if (strategy)
				call.strategy = strategy;
			if (queue != null)
				call.queue = queue;
			call.lock();
			startCall(call);
		}
		
		public function registerConnection(connection:ConnectionManager, Default:Boolean = false):void {
			connections[connection.id] = connection;
			if (Default)
				defaultConnection = connection;
				
			ChangeWatcher.watch(connection, "authenticated", authenticatedHandler);
			ChangeWatcher.watch(connection,"connected", connectedHandler);
		}
		
		// --------------------- VARIABLES -----------------------
		
		private var connections:Object = {};
		private var defaultConnection:ConnectionManager;
		private var queues:Object = {};
		
		// --------------------- QUEUING FUNCTIONS ------------------------
		
		
		/**
		 * This is where every call first comes in.
		 */
		private function startCall(call:RemoteCall):void {
			if (call.strategy == RemoteCall.BYPASS) {
				call.fire(bypassCallback);
				return;
			}
			
			var queue:Queue = getQueue(call.queue, true);
			if (checkQueuing(queue, call.strategy)) {
				if (!tryCall(call))
					wait(call);
			} else {
				wait(call);
			}
		}
		
		/**
		 * Check to see if a call at the given position in the given queue with the given
		 * strategy should be able to be run at this time based soley on queuing strategy.
		 * (i.e. connection issues are not taken into account here.)
		 */
		private function checkQueuing(queue:Queue, strategy:String, position:int = -1):Boolean {
			if (position == -1)
				position = queue.waiting.length;
			
			if (strategy == RemoteCall.ORDERED) {
				if (position == 0 && queue.outstanding.length == 0) {
					return true;
				} else {
					return false;
				}
			} else {
				if (strategy == RemoteCall.OVERWRITE) {
					//Find last ORDERED or OVERWRITE in queue
					for (var i:int = position - 1; i >= 0; i--) {
						var strat:String = RemoteCall(queue.waiting[i]).strategy
						if (strat == RemoteCall.ORDERED) {
							return false;
						} else if (strat == RemoteCall.OVERWRITE) {
							//remove the previous OVERWRITE call
							removeFromQueue(queue, queue.waiting[i]);
							position--;
							break;
						}
					}
				}
				
				var i:int;
				for (i = position - 1; i >= 0; i--) {
					if (RemoteCall(queue.waiting[i]).strategy == RemoteCall.ORDERED) {
						return false;
					}
				}
				
				for each (var call:RemoteCall in queue.outstanding) {
					if (call.strategy == RemoteCall.ORDERED ||
						(call.strategy == RemoteCall.OVERWRITE && strategy == RemoteCall.OVERWRITE)) {
						return false;
					}
				}
				
				return true;
			}
		}
		
		/**
		 * Attempt to fire the call. If there is connection or authentication
		 * trouble, inform the connection manager. This does not check for
		 * queuing strategy requirement.
		 * 
		 * Returns true if the call is fired, false if it is not.
		 */
		private function tryCall(call:RemoteCall):Boolean {
			var connection:ConnectionManager = getConnection(call.connection);
			if (connection) {
				if (connection.connected) {
					if (connection.authenticated) {
						//connected & authenticated
						fire(call);
						return true;
					} else {
						if (!connection.authenticating)
							connection.authenticate();
						return false;
					}
				} else {
					//disconnected
					return false;
				}
			} else {
				//no connection manager
				fire(call);
				return true;
			}
		}
		
		/**
		 * Simply fire the call. No prerequisites are checked.
		 * The call is also moved from the waiting queue to the
		 * outstanding queue if applicable. If the call was not in
		 * a queue before, it is also added to the 'all' queue.
		 */
		private function fire(call:RemoteCall):void {
			var queue:Queue = getQueue(call.queue, true);
			addToQueue(queue, call, true);
			call.fire(callback);
		}
		
		/**
		 * This adds a new call to the end of a queue. This is called when
		 * a call first comes in and cannot be immediately fired.
		 */
		private function wait(call:RemoteCall):void {
			var queue:Queue = getQueue(call.queue, true);
			addToQueue(queue, call, false);
		}
		
		/**
		 * This is the callback function passed to a call when it is fired.
		 * 
		 * Update the connection status if applicable. If the call completed,
		 * pass it the the finish() method, remove the call from the queue and
		 * fire off any other calls that were waiting on it.
		 */
		private function callback(call:RemoteCall, outcome:String):void {
			var connection:ConnectionManager = getConnection(call.connection);
			var queue:Queue = getQueue(call.queue);
			var requeue:Boolean;
			
			if (!connection) {
				//If no connection, all faults are treated as application faults
				finish(call);
			} else {
				if (outcome == "connectionFault") {
					if (connection.connected) {
						if (connectionTestCall) {
							requeue = true;
						} else {
							connectionTestCall = call;
							_reconnectStrategy.start();
						}
						connection.connected = false;
					} else if (connectionTestCall == call) {
						_reconnectStrategy.next();
					} else {
						requeue = true;
					}
				} else {
					if (!connection.connected) {
						if (connectionTestCall) {
							_reconnectStrategy.stop();
							connectionTestCall = null;
						}
						connection.connected = true;
					}
					if (outcome == "authenticationFault") {
						if (connection.authenticated) {
							connection.authenticated = false;
							connection.authenticate();
						}
						requeue = true;
					} else {
						finish(call);
					}
				}
				
				if (requeue) {
					queue.removeFromOutstanding(call);
					queue.waiting.unshift(call);
				}
			}
			
			//--fire off any other calls that were waiting for this call in the queue--
			//create a copy because the waiting queue might change as we go
			var cpy:Array = FlexUtils.copyArray(queue.waiting);
			
outerLoop:	for (var i:int = 0; i < cpy.length; i++) {
				var c:RemoteCall = cpy[i] as RemoteCall
				//find the call in the waiting queue
				for (var j = i; j >= 0; j--) {
					if (queue.waiting[j] == c) {
						if (checkQueuing(queue, c.strategy, j)) {
							if (!tryCall(c) && c.strategy == RemoteCall.ORDERED) {
								break outerLoop; //save some cycles; nothing can go after a blocked ORDERED
							}
						}
						break;
					}
				}
			}
			
		}
		
		/**
		 * This callback is used for calls with 'bypass' strategy.
		 */
		private function bypassCallback(call:RemoteCall, outcome:String):void {
			call.applyOutcome();
		}
		
		/**
		 * Clean up a completed call. Apply the outcome immediately or as
		 * required by the 'returnOrdered' strategy.
		 */
		private function finish(call:RemoteCall):void {
			var queue:Queue = getQueue(call.queue);
			queue.removeFromOutstanding(call);
			
			if (call.strategy == RemoteCall.RETURN_ORDERED) {
				if (queue.all[0] == call) {
					removeFromQueue(queue, call);
					call.applyOutcome();
				} else {
					//we can't apply outcome until all previous calls are finished
					return;
				}
			} else {
				for (var i:int = 0; i < queue.all.length; i++) {
					if (queue.all[i] == call) {
						removeFromQueue(queue, call);
						call.applyOutcome();
						break;
					} else if (RemoteCall(queue.all[i]).strategy == RemoteCall.RETURN_ORDERED) {
						//we can't apply the outcome until all RETURN_ORDERED calls
						//made before are finished
						return;
					}
				}
			}
			
			//finish any calls that may have been waiting for this to finish
			for each (var c:RemoteCall in queue.all) {
				if (isFinished(queue, c))
					finish(c);
			}
		}
		
		/**
		 * If outstanding == true, add call to outstanding queue; otherwise add to waiting queue.
		 * If it is already in the correct queue, do nothing. If it is in the other queue, move it.
		 * Also adjust the all queue as needed.
		 */
		private function addToQueue(queue:Queue, call:RemoteCall, outstanding:Boolean):void {
			var oldValue:Array = FlexUtils.copyArray(queue.all);
			
			var i:int;
			var found:Boolean = (-1 != FlexUtils.indexOfStrict(queue.all, call));
			
			var rightQ:Array = (outstanding ? queue.outstanding : queue.waiting) as Array;
			if (found) {
				//the call is in the 'all' queue. It may also be in waiting, outstanding or neither.
				var wrongQ:Array = (outstanding ? queue.waiting : queue.outstanding) as Array;
				
				//remove from wrong queue if found
				var foundInWrongQ:Boolean = (-1 != FlexUtils.removeItemStrict(wrongQ, call));
				
				if (foundInWrongQ) {
					//if was found in the wrong queue, put it in the right queue
					rightQ.push(call);
				} else {
					//if it wasn't in the wrong queue, it may already be in the right queue
					var foundInRightQ:Boolean = (-1 != FlexUtils.indexOfStrict(rightQ, call));
					//if it's not already in the right queue, put it there
					if (!foundInRightQ) {
						rightQ.push(call);
					}
				}
			} else {
				//the call is not in any of the queues
				rightQ.push(call);
				queue.all.push(call);
				
				_pendingCallsByQueue.dispatchEvent(
					PropertyChangeEvent.createUpdateEvent(_pendingCallsByQueue, queue.id, oldValue, FlexUtils.copyArray(queue.all)));
				
				_pendingCalls.addItem(call);
			}
		}
		
		/**
		 * Remove from the queue if found.
		 */
		private function removeFromQueue(queue:Queue, call:RemoteCall):void {
			var oldValue:Array = FlexUtils.copyArray(queue.all);
			
			//remove from 'all' queue
			var found:Boolean = (-1 != FlexUtils.removeItemStrict(queue.all, call));
			
			if (found) {
				//if it was in the 'all' queue, it may also be in one of the others
				var foundInOutstandingQ:Boolean = (-1 != FlexUtils.removeItemStrict(queue.outstanding, call));
				if (!foundInOutstandingQ) {
					//if it wasn't in outstanding, it could be in waiting.
					FlexUtils.removeItemStrict(queue.waiting, call)
				}
				
				var event:PropertyChangeEvent = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.UPDATE);
				event.source = _pendingCallsByQueue;
				event.property = queue.id;
				event.oldValue = oldValue;
				event.newValue = FlexUtils.copyArray(queue.all);
				_pendingCallsByQueue.dispatchEvent(event);
				
				FlexUtils.removeItemStrict(_pendingCalls, call);
			}		
		}
		
	//---------- Connections and Authentication -------------
		
		/**
		 * Called when the authentication status of a connection changes.
		 * 
		 * If a connection has become authenticated, try to fire off any calls
		 * that have been waiting for it.
		 */
		private function authenticatedHandler(event:PropertyChangeEvent):void {
			if (event.newValue == true) {
				var connection:ConnectionManager = event.target as ConnectionManager;
				
				for each (var queue:Queue in queues) {
					
					//--fire off any other calls that were waiting for this call in the queue--
					//create a copy because the waiting queue might change as we go
					var cpy:Array = FlexUtils.copyArray(queue.waiting);
					
cpyLoop:			for (var cpyIndex:int = 0; cpyIndex < cpy.length; cpyIndex++) {
						var c:RemoteCall = cpy[cpyIndex] as RemoteCall
						if (getConnection(c.connection) === connection) {
							//find the call in the waiting queue
							for (var i = cpyIndex; i >= 0; i--) {
								if (queue.waiting[i] == c) {
									if (checkQueuing(queue, c.strategy, i)) {
										if (!tryCall(c) && c.strategy == RemoteCall.ORDERED) {
											break cpyLoop; //save some cycles; nothing can go after a blocked ORDERED
										}
									}
									break;
								}
							}
						} else if (c.strategy == RemoteCall.ORDERED) {
							break; //save some cycles; nothing can go after a blocked ORDERED
						}
					}
				}
				
			}
		}
		
		private var connectionTestCall:RemoteCall = null;
		private var _reconnectStrategy:ReconnectStrategy;
		
		private function connectedHandler(event:PropertyChangeEvent):void {
			//At least for now, these do exactly the same thing.
			authenticatedHandler(event);
		}

		public function testConnection():void {
			connectionTestCall.fire(callback);
		}

	//-------- Publishing status ---------
	private var _pendingCalls:ArrayCollection = new ArrayCollection();
	private var _pendingCallsByQueue:PendingCallsByQueue;
	private var _erroredCalls:ArrayCollection = new ArrayCollection();
	
	public function get pendingCalls():ArrayCollection {
		return _pendingCalls;
	}
	
	public function get pendingCallsByQueue():PendingCallsByQueue {
		return _pendingCallsByQueue;
	}
	
	public function get erroredCalls():ArrayCollection {
		return _erroredCalls;
	}
	
	public function clearErroredCalls():void {
		erroredCalls.removeAll();
	}
		
	//-------- Helper Utility Functions ---------
		
		private function getQueue(queueId:String, create:Boolean = false):Queue {
			if (!queueId)
				queueId = "DEFAULT";
			var queue:Queue = queues[queueId];
			if (create && !queue) {
				queue = new Queue();
				queue.id = queueId;
				queues[queueId] = queue;
			}
			return queue;
		}
		
		private function getConnection(connectionId:String):ConnectionManager {
			if (!connectionId) {
				return defaultConnection;
			} else {
				return connections[connectionId];
			}
		}
		
		private function isFinished(queue:Queue, call:RemoteCall):Boolean {
			var c:RemoteCall;
			for each (c in queue.waiting) {
				if (c == call)
					return false;
			}
			for each (c in queue.outstanding) {
				if (c == call)
					return false;
			}
			return true;
		}
		
		public function debug():String {
			var s:String = '';
			for each (var q:Queue in queues) {
				s += q;
			}
			return s;
		}
	}
}
	import flash.utils.Proxy;
	import flash.events.IEventDispatcher;
	import flash.events.EventDispatcher;
	import flash.events.Event;
	import flash.utils.flash_proxy;
	import com.trg.flex.remote.Queue;
	import com.trg.flex.FlexUtils;

[Event(name="propertyChange", type="mx.events.PropertyChangeEvent")]
[Bindable("propertyChange")]
dynamic class PendingCallsByQueue extends Proxy implements IEventDispatcher {
	public function PendingCallsByQueue(queues:Object) {
		super();
		_eventDispatcher = new EventDispatcher(this);
		_queues = queues;
	}

	//Queue proxying
	private var _queues:Object;
	
	override flash_proxy function getProperty(name:*):* {
		var queue:Queue = _queues[name];
		if (queue) {
			return FlexUtils.copyArray(queue.all);
		} else {
			return [];
		}
	}
	
	override flash_proxy function hasProperty(name:*):Boolean {
		return true;
	}
	
	private var _item:Array; // array of object's properties
	
	override flash_proxy function nextNameIndex (index:int):int {
		// initial call
		if (index == 0) {
			_item = new Array();
			for (var x:* in _queues) {
				_item.push(x);
			}
		}
		
		if (index < _item.length) {
			return index + 1;
		} else {
			return 0;
		}
	}
	
	override flash_proxy function nextName(index:int):String {
		return _item[index - 1];
	}
	
	override flash_proxy function nextValue(index:int):* {
		return flash_proxy::getProperty(flash_proxy::nextName(index));
	}
	
	
	//Event Dispatcher proxying
	private var _eventDispatcher:EventDispatcher;
	
	public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void {
		_eventDispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
	}
	
	public function dispatchEvent(event:Event):Boolean {
		return _eventDispatcher.dispatchEvent(event);
	}
	
	public function hasEventListener(type:String):Boolean {
		return _eventDispatcher.hasEventListener(type);
	}
	
	public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void {
		_eventDispatcher.removeEventListener(type, listener, useCapture);
	}
	
	public function willTrigger(type:String):Boolean {
		return _eventDispatcher.willTrigger(type);
	}
}
//Remaining...
//- when connection reestablished
//- when auth established
//- authenticationRequired = false
//- call groups

//Other...
//- progress widgets
//- remote search to model
//- object for id?
//- object for id in ModelCache